
#include <signal.h>
#include <sys/socket.h>
#include <strings.h>		// bzero()
#include <arpa/inet.h>   	// inet_ntop()
#include <netinet/in.h>		// inet_addr()
#include <sys/types.h>
#include <sys/select.h>		// select()
#include <sys/time.h>		// timeval
#include <netdb.h>
#include <unistd.h>

#include <cstdlib> 			// EXIT_xxx
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
#include <functional>		// std::ref()

#include "websocket.h"

#include "teletype.h"

//------------------------------------------------------------------------------------------------------------------
typedef std::vector<std::thread> thread_vec_t;

//------------------------------------------------------------------------------------------------------------------
static std::atomic<int> g_terminate(0);

//------------------------------------------------------------------------------------------------------------------
static void signal_handler(int sig)
{
	std::cout.sync_with_stdio(true);	// Make sure std::cout is thread-safe
	
	std::cout << "[server/WARN] caught signal = [" << strsignal(sig) << "]\n";  // Unsafe. strsignal (is non-reentrant)	
	
	if (SIGINT == sig || SIGTERM == sig) {
		std::cout << "[server/WARN] shutdown\n";
		g_terminate = 1;
	}
}
//------------------------------------------------------------------------------------------------------------------
void treat_remote_client(client_type cli_type)
{
	if (g_terminate)
		return; 
	
	std::cout.sync_with_stdio(true);	// Make sure std::cout is thread-safe
	
    struct addrinfo hints;
    struct addrinfo *result;
    
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_canonname = NULL;
    hints.ai_addr      = NULL;
    hints.ai_next      = NULL;
    hints.ai_socktype  = SOCK_STREAM;
    hints.ai_family    = AF_UNSPEC;        /* Allows IPv4 or IPv6 */
    hints.ai_flags     = AI_PASSIVE;        /* Use wildcard IP address */
	
	int s = getaddrinfo(NULL, "12002", &hints, &result);
	if (0 != s) {
    	g_terminate = 1;
    	return;		
	}
	sockfd_t sockfd = -1;
	struct addrinfo * rp;
	for (rp = result; rp != NULL; rp = rp->ai_next) {
	    if ((sockfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol)) == -1)
	    	continue;	// Try next address.
	    
	    int yes = 1;    
	    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) {
	        	std::cout << "[server/FATAL] setsockopt [" << sockfd << "] failed - errno(" << strerror(errno) << ")\n";
	        	close(sockfd);
	        	freeaddrinfo(result);
	        	g_terminate = 1;
	        	return;    	
	    }
	    
	    if (bind(sockfd, rp->ai_addr, rp->ai_addrlen) == 0)
	    	break; 	// Success.
	    
	    // Bind failed: close this socket and try next address.
	    
	    close(sockfd);
	}    
    if (rp != NULL) {
    	if (listen(sockfd, 0) == -1) {
			std::cout << "[server/FATAL] listen for socket [" << sockfd << "] failed - errno(" << strerror(errno) << ")\n";
			close(sockfd);
			g_terminate = 1;
			freeaddrinfo(result);
			return;
    	}
    }
    std::cout << "[server/DEBUG] waiting for clients on port [" << static_cast<int>(cli_type) << "] ...\n";
    
    thread_vec_t threads;        
    fd_set read_fds;    
    fd_set cli_fds;    
    FD_ZERO(&read_fds);
    FD_ZERO(&cli_fds);    
    FD_SET(sockfd, &read_fds);		// Add the socket descriptor to the master set   
    
    int fdmax = sockfd;
    int cli_fdmax = 0;
    
    struct timespec timeout;
    timeout.tv_sec  = 2;
    timeout.tv_nsec = 0;
    
    while (! g_terminate) {    	
    	
    	int ret = pselect(fdmax + 1, &read_fds, NULL, NULL, &timeout, NULL);
    	
    	if (ret < 0 && errno != EINTR) {
        	std::cout << "[server/FATAL] select()\n";
        	g_terminate = 1;
        	break;
    	}    	
    	else if (ret == 0) { // Nothing to read. No valid descriptor (sockfd). 
    		FD_SET(sockfd, &read_fds);
    		continue; 
    	}
    	else if (g_terminate)
    		break;
    	
    	if (FD_ISSET(sockfd, &read_fds)) {			
			sock_info_t remote;
			socklen_t sin_size = sizeof(sock_info_t);
			
			std::cout << "on accept\n";
			sockfd_t cli_sockfd = accept(sockfd, (struct sockaddr *)&remote, &sin_size);
			if (cli_sockfd < 0) {
				std::cout << "[server/ERROR] accept failed - errno(" << strerror(errno) << ")\n";
				continue;
			}    		    	
			FD_SET(cli_sockfd, &cli_fds);
			cli_fdmax = cli_sockfd;
			
			std::string ip(inet_ntoa(remote.sin_addr));
			std::cout << "[server/DEBUG] connected " << cli_sockfd << " " << ip 
					  << ":" << ntohs(remote.sin_port) << std::endl;
			
			teletype tty(cli_type, g_terminate, cli_sockfd, ip);
			threads.push_back(std::thread{&teletype::run, &tty});
		}			
    }    
    std::cout << "[server/DEBUG] closing server descriptor [" << sockfd << "]\n";
    close(sockfd);

    for (int i = 0; i <= cli_fdmax; ++i) {    	
    	if (FD_ISSET(i, &cli_fds)) {
    		std::cout << "[server/DEBUG] closing client descriptor [" << i << "]\n";
    		close(i);
    	}
    }
    std::cout << "[server/DEBUG] terminate\n";
    for (auto & t : threads) {
    	t.join();
    	std::cout << "[server/DEBUG] thread joined...\n";
    }
}

//------------------------------------------------------------------------------------------------------------------
int main(int argc, char * argv[])
{   	
    struct sigaction sa;
    
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sa.sa_handler = signal_handler;
    
    // Control-\ (the SIGQUIT) on terminal generates a core dump. This is useful to break an infinite loop program. 
    // This signal will not be treaded in order to get a coredump.
    
    if (sigaction(SIGINT, &sa, NULL) == -1) {		// Control-C
    	std::cout << "[server/FATAL] sigaction failed for SIGINT\n";
    	return EXIT_FAILURE;
    }    
    if (sigaction(SIGTERM, &sa, NULL) == -1) {		// terminates gracefully, however SIGKILL(punk method) will bypass it.
    	std::cout << "[server/FATAL] sigaction failed for SIGTERM\n";
    	return EXIT_FAILURE;    	
    }

    treat_remote_client(client_type::board);
    //std::thread web_thr{treat_remote_client, client_type::web};
    //std::thread board_thr{treat_remote_client, client_type::board};
    
    //web_thr.join();
    //board_thr.join();
    
    return EXIT_SUCCESS;
}
